package framework.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.log4j.Logger;

import common.Configuration;
import common.LogCategory;

import framework.Trait;
import framework.Message;
import framework.Service;
import framework.server.cfg.ParamCfg;
import framework.server.cfg.ServiceCfg;

/**
 * The class loader of the framework
 * <p>
 * loads classes of services
 *
 * @author      Orlin Tomov
 * @version     0.2
 *
 */
public class Loader extends ClassLoader {

	private static final Logger log = LogCategory.getCategory(Loader.class);
	/** classes that are loaded */
	private Map<String, Class> classes = new Hashtable<String, Class>();
	/** path name to the jar files */
	private String pathName = null;
	/** the names of the jar files where the classes are loaded from */
	private Map<String, Long> jarNames = new Hashtable<String, Long>();
	/** boolean handler that shows if a jar file is specified to load the relevant class from */
	private boolean hasJarName = false;

	/**
	 * Construct the loader
	 *
	 */
	public Loader() {
		super(ClassLoader.getSystemClassLoader());
	}

	/**
	 * Get classes
	 *
	 * @return	classes Hashtable representing loaded classes and their data
	 */
	public Map<String, Class> getClasses() {
		return classes;
	}

	/**
	 * Get path name
	 *
	 * @return	pathName String representation of the path for the jar files
	 */
	public String getPathName() {
		return pathName;
	}

	/**
	 * Get jar names
	 *
	 * @return	jarNames Hashtable
	 */
	public Map<String,Long> getJarNames() {
		return jarNames;
	}

	public void register(String pathName, String jarName) {
		String path;
		if (pathName == null) {
			path = Configuration.formDefaultPath("", "");
		} else {
			path = Configuration.formDefaultPath(pathName, "");
		}
		if (jarName != null) {
			if (jarName.split(File.pathSeparator).length > 0) {
				for (String name: jarName.split(File.pathSeparator)) {
					File jarFile = new File(path + name);
					jarNames.put(name, new Long(jarFile.lastModified()));
				}
			} else {
				File jarFile = new File(path + jarName);
				jarNames.put(jarName, new Long(jarFile.lastModified()));
			}
		}
	}

	/**
	 * Loads class and instantiate it
	 *
	 * @param	server		server pointer
	 * @param	serviceCfg	the presents of a service 
	 * @return	instance of the started service
	 */
	public Service getService(Trait server, ServiceCfg serviceCfg) {
		this.pathName = serviceCfg.getPath();
		String path;
		if (pathName == null) {
			path = System.getProperty("user.dir") + File.separator;
		} else {
			path = System.getProperty("user.dir") + File.separator + pathName + File.separator;
		}
		hasJarName = false;
		if (serviceCfg.getJar() != null) {
			hasJarName = true;
			if (serviceCfg.getJar().split(File.pathSeparator).length > 0) {
				for (String name: serviceCfg.getJar().split(File.pathSeparator)) {
					File jarFile = new File(path + name);
					jarNames.put(name, new Long(jarFile.lastModified()));
				}
			} else {
				File jarFile = new File(path + serviceCfg.getJar());
				jarNames.put(serviceCfg.getJar(), new Long(jarFile.lastModified()));
			}
		}
		Service instance = null;
		try {
			Class<Service> service;
			if (System.getProperty("loader")==null) {
				service = (Class<Service>) Class.forName(serviceCfg.getClassName());
			} else {
				service = (Class<Service>) loadClass(serviceCfg.getClassName());
			}
			Class[] params = new Class[] {Class.forName("framework.Trait"), serviceCfg.getName().getClass()};
			Constructor<Service> constructor = service.getDeclaredConstructor(params);
			instance = constructor.newInstance(new Object[] {server, serviceCfg.getName()});
			setParams(instance, serviceCfg.getParams());
			if (serviceCfg.isRunnig()) {
				instance.starting();
			}
		} catch (ClassNotFoundException exception) {
			/**/log.error("Class "+serviceCfg.getClassName()+" could not be loaded!", exception);
		} catch (IllegalArgumentException exception) {
			/**/log.error("Class "+serviceCfg.getClassName()+" could not be instanted!", exception);
		} catch (SecurityException exception) {
			/**/log.error("Class "+serviceCfg.getClassName()+" could not be instanted!", exception);
		} catch (InstantiationException exception) {
			/**/log.error("Class "+serviceCfg.getClassName()+" could not be instanted!", exception);
		} catch (IllegalAccessException exception) {
			/**/log.error("Class "+serviceCfg.getClassName()+" could not be instanted!", exception);
		} catch (InvocationTargetException exception) {
			/**/log.error("Class "+serviceCfg.getClassName()+" could not be instanted!", exception);
		} catch (NoSuchMethodException exception) {
			/**/log.error("Class "+serviceCfg.getClassName()+" could not be instanted!", exception);
		}
		return instance;
	}

	/**
	 * Loads the class with the specified name
	 *
	 * @param	name	the name of the class
	 * @param	resolve	if true then resolve the class 
	 * @return	the resulting Class object 
	 * @throws	ClassNotFoundException if the class could not be found
	 */
	@Override
	protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		//log.debug("looking for: " + name);
		Class<?> result = classes.get(name);
		if(result != null) {
			//log.debug("found in classes: " + name);
			return result;
		}
		try {
			result = super.findSystemClass(name);
			//log.debug("found in system: " + name);
			return result;
		} catch (ClassNotFoundException e) {
			byte classData[] = getClassData(name);
			if (classData == null) {
				throw new ClassNotFoundException();
			}
			//log.debug("load:"+name);
			result = defineClass(name, classData, 0, classData.length);
		}
		if (resolve) {
			resolveClass(result);
		}
		classes.put(name, result);
		return result;
	}

	/**
	 * Loads class and instantiate it
	 *
	 * @param	name	java class name
	 * @return	binary data of the loaded class
	 */
	private byte[] getClassData(String name) {
		byte classData[] = null;
		String className = name.replace('.', '/') + ".class";
		String path;
		if (pathName == null) {
			path = Configuration.formDefaultPath("", "");
		} else {
			path = Configuration.formDefaultPath(pathName, "");
		}
		if (!hasJarName) {
			//log.debug(path + className);
			File file = new File(path + className);
			try {
				InputStream in = new FileInputStream(file);
				classData = new byte[(int)file.length()];
				int bytes = in.read(classData, 0, (int)file.length());
				if (bytes != classData.length) {
					/**/log.warn("Bad file size!!!");
				}
			} catch(IOException exception) {
				/**/log.error("Could not read file: " + path + className + ": ", exception);
			}
		} else {
			boolean isClassHere = false;
			for (String jarName: jarNames.keySet()) {
				try {
					File jarFile = new File(path + jarName);
					ZipFile zip = new ZipFile(jarFile);
					isClassHere = false;
					Enumeration<?> entries = zip.entries();
					while(entries.hasMoreElements()) {
						ZipEntry entry = (ZipEntry) entries.nextElement();
						//log.debug("entry:"+entry.getName());
						if (entry.getName().equals(className)) {
							isClassHere = true;
							InputStream in = zip.getInputStream(entry);
							classData = new byte[(int)entry.getSize()];
							int bytes = in.read(classData, 0, (int)entry.getSize());
							if (bytes != classData.length) {
								/**/log.warn("Bad file size!!! (read " + bytes + " of " + classData.length + ")");
							}
							if (classData != null) {
								/**/log.debug("load from " + jarName + ": " + name);
							}
							jarNames.put(jarName, new Long(jarFile.lastModified())); 
							break;
						}
					}
				} catch(IOException exception) {
					/**/log.error("Could not read file: "+jarName, exception);
				}
				if(isClassHere) {
					return classData;
				}
			}
		}
		return classData;
	}

	/**
	 * Sets properties of the instance
	 *
	 * @param	instance	the instance
	 * @param	params	the map with names and values of the prporties
	 */
	private void setParams(Service instance, List<ParamCfg> params) {
		if (params == null) {
			return;
		}
		for (ParamCfg param : params) {
			String paramName = param.getName();
			Object paramVal = param.getValue();
			try {
				String setterName = "set" + paramName.substring(0, 1).toUpperCase() + paramName.substring(1);
				Method setter = null;
				try {
					setter = instance.getClass().getMethod(setterName, new Class[] {String.class});
					setter.invoke(instance, paramVal);
					continue;
				} catch (NoSuchMethodException exception) {
					/** do noting, will try with int param */
				}
				/** try int param */
				try {
					setter = instance.getClass().getMethod(setterName, new Class[] {Integer.class});
					int paramInt = Integer.parseInt((String) paramVal);
					setter.invoke(instance, paramInt);
					continue;
				} catch (NoSuchMethodException exception) {
					/** do noting, will try with int param */
				}
				/** try bool param */
				setter = instance.getClass().getMethod(setterName, new Class[] {Boolean.class});
				boolean paramBool = Boolean.parseBoolean((String) paramVal);
				setter.invoke(instance, paramBool);
			} catch (NoSuchMethodException exception) {
				/**/log.error(exception.getMessage(), exception);
			} catch(IllegalAccessException exception) {
				/**/log.error(exception.getMessage(), exception);
			} catch (SecurityException exception) {
				/**/log.error(exception.getMessage(), exception);
			} catch (IllegalArgumentException exception) {
				/**/log.error(exception.getMessage(), exception);
			} catch (InvocationTargetException exception) {
				/**/log.error(exception.getMessage(), exception);
			}
		}
	}

	public Message getMessage(String className, Object object) {
		Message message = null;
		Class<Message> messageClass = null;
		try {
			messageClass = (Class<Message>) loadClass(className);
			message = messageClass.getConstructor(new Class[] {object.getClass()}).newInstance(new Object[] {object});
		} catch (ClassNotFoundException exception) {
			/**/log.error("Class "+className+" could not be loaded!", exception);
		} catch (IllegalArgumentException exception) {
			/**/log.error("Class "+className+" could not be instanted!", exception);
		} catch (SecurityException exception) {
			/**/log.error("Class "+className+" could not be instanted!", exception);
		} catch (InstantiationException exception) {
			/**/log.error("Class "+className+" could not be instanted!", exception);
		} catch (IllegalAccessException exception) {
			/**/log.error("Class "+className+" could not be instanted!", exception);
		} catch (InvocationTargetException exception) {
			/**/log.error("Class "+className+" could not be instanted!", exception);
		} catch (NoSuchMethodException exception) {
			/**/log.error("Class "+className+" could not be instanted!", exception);
		}
		return message;
	}
}
